((global, factory) => {
  typeof exports === "object" && typeof module !== "undefined"
    ? (module.exports = factory(global))
    : typeof define === "function" && define.amd
    ? define(() => factory(global))
    : ((global = typeof globalThis !== "undefined" ? globalThis : global),
      (global.mfoRouter = factory(global)));
})(window, (global) => {
  const DEFAULTROUTEPATH = "/";
  const objToURI = (obj) => {
    let str = [];
    Object.keys(obj).forEach((n) =>
      str.push([n, "=", encodeURIComponent(obj[n])].join(""))
    );
    return str.join("&");
  };
  const objToParams = (obj, params) => {
    obj = obj.split("/");
    obj.forEach((item, i) => {
      params[item] && obj.splice(i, 1, params[item]);
    });
    return obj.join("/");
  };
  const paramsToObj = (URI, temp) => {
    let params = {};
    temp = temp.split("/");
    URI.split("/").forEach((item, i) => i > 1 && (params[temp[i]] = item));
    return params;
  };
  class RouteMap {
    constructor() {
      return this;
    }
    _routeMap = {};
    get routeMap() {
      return this._routeMap;
    }
    set routeMap(ops) {
      const that = this;
      ops.routes.forEach((route) => {
        that._routeMap[route.path] = new Route(route);
      });
    }
  }
  class Router {
    constructor(ops) {
      this.routeMap = new RouteMap();
      this.routeError = new RouterError({ root: this });
      this.options = ops || {};
      this._init();
      return this;
    }
    forceUpdate() {
      this.resetCurrent();
      return this;
    }
    _routerBeforeEach = () => {
      return Promise.resolve();
    };
    _routerAfterEach = () => {};
    _current = undefined;
    get current() {
      return this.hasRoute(this._current);
    }
    set current(ops) {
      const that = this;
      this.verifyRouteAndJump(ops, (map, route) => {
        that.from = map[that._current];
        that._current = route;
        that.to = map[route];
        return Promise.resolve();
      });
    }
    hasRoute(ops) {
      if (typeof ops !== "string") return;
      let map = this.routeMap.routeMap,
        isHas = undefined;
      const mapKeys = Object.keys(map);
      for (let i = 0; i < mapKeys.length; i++) {
        const route = mapKeys[i];
        if (
          ops === route ||
          (typeof ops === "string" &&
            ops.split("/").length >= 2 &&
            route.indexOf(ops.split("/")[1])) === 1 ||
          (/\?/.test(ops) && route === ops.split("?")[0])
        ) {
          isHas = map[route];
          break;
        }
      }
      return isHas;
    }
    verifyRouteAndJump(ops, filter) {
      const that = this;
      let map = this.routeMap.routeMap;
      let route = this.hasRoute(ops);
      if (route) {
        const path = route.path;
        filter(map, path).then(() =>
          this._routerBeforeEach()
            .then((res) => {
              if (res) {
                that.push(res);
                return;
              }
            })
            .then(() => {
              return new Promise((resolve) => {
                if (typeof map[path].redirect === "string") {
                  that.push(map[path].redirect);
                  return;
                } else if (typeof map[path].redirect === "function") {
                  that.push(map[path].redirect(that.from, that.to));
                  return;
                } else {
                  map[path].render(that.from, that.to);
                }
                resolve();
              });
            })
            .then(() => that._routerAfterEach())
        );
      } else {
        this.routeError.error = { path: ops, isHas: route, routes: map };
      }
    }
    mode = "hash";
    _options = {};
    get options() {
      return this._options;
    }
    set options(ops) {
      if (!ops) return;
      Object.assign(this._options, ops || {});
      this.createRouterMap();
    }
    _init() {
      this.mode = this.options.mode || "hash";
      this._modeEvent(true);
      this._modeEvent();
      this.createRouterMap();
    }
    resetCurrent() {
      this._modeEventHandle(this.mode === "hash" ? false : true);
      return this;
    }
    _modeEventHandle(bool) {
      const path = bool
        ? global.location.pathname
        : global.location.hash.substr(1);
      this.current = path || DEFAULTROUTEPATH;
    }
    _modeEventBeforeUnloadHandle() {
      const { history } = global;
      if (!history.state) return;
      history.replaceState(assign({}, history.state), "");
    }
    _modeEvent(bool) {
      const that = this;
      global[!bool ? "addEventListener" : "removeEventListener"](
        this.mode === "hash" ? "hashchange" : "popstate",
        !bool
          ? () => {
              that._modeEventHandle(this.mode === "hash" ? false : true);
            }
          : () => {}
      );
      global[!bool ? "addEventListener" : "removeEventListener"](
        "beforeunload",
        that._modeEventBeforeUnloadHandle(),
        !bool
          ? false
          : {
              passive: true,
            }
      );
      return this;
    }
    createRouterMap() {
      this.routeMap.routeMap = this.options;
      return this;
    }
    addRoute(ops) {
      this.options.routes.push(ops);
      this.createRouterMap();
      return this;
    }
    removeRoute(ops) {
      const that = this;
      this.options.routes.forEach((route, i) => {
        if (ops === route.name || ops === route.path) {
          that.options.routes.splice(i, 1);
          delete that.routeMap.routeMap[route.path];
        }
      });
      return this;
    }
    updateRoute(ops) {
      const that = this;
      const updateRoute = ops; //new Route(ops);
      const isHas = that.hasRoute(updateRoute.path);
      if (!isHas) {
        that.addRoute(ops);
      } else {
        Object.assign(that.routeMap.routeMap[updateRoute.path], updateRoute);
      }
    }
    beforeEach(callback) {
      const that = this;
      this._routerBeforeEach = () =>
        new Promise((resolve) => callback(that.from, that.to, resolve));
      return this;
    }
    afterEach(callback) {
      const that = this;
      this._routerAfterEach = () => callback(that.from, that.to);
      return this;
    }
    routeJumpErrorFeedBack(callback) {
      this.routeError.routeError = (err) => callback(err, this.error);
      return this;
    }
  }
  class RouterError {
    constructor(ops) {
      this.options = ops;
      return this;
    }
    _error = [];
    get error() {
      return this._error;
    }
    set error(err) {
      this._error.push(err);
      this.routeError(err);
    }
    routeError = (err) => {
      console.error(err);
    };
  }

  class Route {
    constructor(ops) {
      Object.assign(this, ops);
      return this;
    }
    name = undefined;
    path = undefined;
    render = () => {};
    redirect = undefined;
    meta = Object.create(null);
    params = undefined;
    query = undefined;
  }

  const hasOwn = {}.hasOwnProperty;
  const isPlainObject = (obj) => {
    var key;
    if (!obj || typeof obj !== "object" || obj.nodeType) {
      return false;
    }
    try {
      if (
        obj.constructor &&
        !hasOwn.call(obj, "constructor") &&
        !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")
      ) {
        return false;
      }
    } catch (e) {
      return false;
    }
    for (key in obj) {
    }
    return key === undefined || hasOwn.call(obj, key);
  };

  class Navigation {
    constructor({ router }) {
      this.root = router;
      return this;
    }
    jumpRoute(name, v) {
      this.root.mode === "hash"
        ? (global.location.hash = v)
        : name === "push"
        ? global.history.pushState(null, "", v)
        : global.history.replaceState(null, "", v);
    }
    modeJumpRoute(name, v) {
      if (!v) return;
      let routeLoaction = undefined;
      if (isPlainObject(v)) {
        const routeQuery = v.query && objToURI(v.query);
        const routeParams = v.params && objToParams(v.path, v.params);
        routeLoaction =
          v.path &&
          (routeQuery
            ? v.path + (/\?/.test(v.path) ? "&" : "?") + routeQuery
            : routeParams
            ? routeParams
            : v.path);
        this.root.updateRoute(v);
        this.jumpRoute(name, routeLoaction);
      } else if (typeof v === "string") {
        routeLoaction = v;
        this.jumpRoute(name, routeLoaction);
      }
    }
    push(v) {
      this.modeJumpRoute("push", v);
      return this;
    }
    back(v) {
      global.history.back();
      return this;
    }
    replace(v) {
      this.modeJumpRoute("replace", v);
      return this;
    }
    go(v) {
      global.history.go(v);
      return this;
    }
  }

  return {
    createRouter(ops) {
      const router = new Router(ops);
      ["push", "back", "replace", "go"].forEach((name) => {
        router[name] = (v) => new Navigation({ router })[name](v);
      });
      return router;
    },
  };
});
